﻿/*
Copyright © 2010 SectorPoint, Inc.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace SectorPoint.Net.Mail
{
	/// <summary>
	/// Client architecture for the Post Office Protocol - Version 3.
	/// </summary>
	public class Pop3Client : IDisposable
	{
		#region Constructor
		/// <summary>
		/// Initializes a new instance of the SectorPoint.Net.Mail.Pop3Client class by using configuration file settings.
		/// </summary>
		public Pop3Client()
		{
			Pop3Section configuration = (Pop3Section)System.Configuration.ConfigurationManager.GetSection( @"/cofiguration/system.net/mailSettings/pop3" );

			string host = configuration.Network.Host;
			int port = configuration.Network.Port;
			bool useSsl = configuration.Network.UseSsl;
			string user = configuration.Network.User;
			string password = configuration.Network.Password;

			if (String.IsNullOrEmpty( host ))
				throw new ArgumentNullException( "host" );
			if (port < 0)
				throw new ArgumentOutOfRangeException( "port", port, "Port must be greater than 0." );
			if (String.IsNullOrEmpty( user ))
				throw new ArgumentNullException( "user" );
			if (String.IsNullOrEmpty( password ))
				throw new ArgumentNullException( "password" );

			this.host = host;
			this.port = port;
			this.useSsl = useSsl;
			tcpClient = null;
			popStream = null;
			State = Pop3State.Authorization;

			this.Initialize();
			this.Authorize( user, password );
		}

		/// <summary>
		/// Initializes a new instance of the SectorPoint.Net.Mail.Pop3Client class by using configuration file settings and the specified user information.
		/// </summary>
		/// <param name="user">User name for the maildrop.</param>
		/// <param name="password">Password for the user.</param>
		public Pop3Client( string user, string password )
		{
			Pop3Section configuration = (Pop3Section)System.Configuration.ConfigurationManager.GetSection( @"/cofiguration/system.net/mailSettings/pop3" );

			string host = configuration.Network.Host;
			int port = configuration.Network.Port;
			bool useSsl = configuration.Network.UseSsl;

			if (String.IsNullOrEmpty( host ))
				throw new ArgumentNullException( "host" );
			if (port < 0)
				throw new ArgumentOutOfRangeException( "port", port, "Port must be greater than 0." );
			if (String.IsNullOrEmpty( user ))
				throw new ArgumentNullException( "user" );
			if (String.IsNullOrEmpty( password ))
				throw new ArgumentNullException( "password" );

			this.host = host;
			this.port = port;
			this.useSsl = useSsl;
			tcpClient = null;
			popStream = null;
			State = Pop3State.Authorization;

			this.Initialize();
			this.Authorize( user, password );
		}

		/// <summary>
		/// Initializes a new instance of the SectorPoint.Net.Mail.Pop3Client class to retrieve email by using the specified POP3 server and user information.
		/// </summary>
		/// <param name="host">Host name of the POP3 server to connect to.</param>
		/// <param name="port">Port number of the POP3 server to connect to.</param>
		/// <param name="user">User name for the maildrop.</param>
		/// <param name="password">Password for the user.</param>
		/// <param name="useSsl">True if connecting using SSL; otherwise, false.</param>
		public Pop3Client(string host, int port, string user, string password, bool useSsl)
		{
			if (String.IsNullOrEmpty( host ))
				throw new ArgumentNullException( "host" );
			if (port < 0)
				throw new ArgumentOutOfRangeException( "port", port, "Port must be greater than 0." );
			if (String.IsNullOrEmpty( user ))
				throw new ArgumentNullException( "user" );
			if (String.IsNullOrEmpty( password ))
				throw new ArgumentNullException( "password" );

			this.host = host;
			this.port = port;
			this.useSsl = useSsl;
			tcpClient = null;
			popStream = null;
			State = Pop3State.Authorization;

			this.Initialize();
			this.Authorize( user, password );
		}
		#endregion

		#region Private Fields
		TcpClient tcpClient;
		Pop3Stream popStream;
		string host;
		int port;
		bool useSsl;
		List<Pop3Message> b_MailMessages;
		#endregion

		#region Public Properties
		/// <summary>
		/// Gets the current state of the POP3 session.
		/// </summary>
		public Pop3State State { get; private set; }

		/// <summary>
		/// Gets a list of the messages associated with this maildrop. TODO: convert to a strongly typed collection
		/// </summary>
		public List<Pop3Message> MailMessages
		{
			get
			{
				if (b_MailMessages == null)
				{
					b_MailMessages = new List<Pop3Message>();
					SendCommand( Pop3Command.List );

					byte[] content = popStream.Read( true, true, false );
					if (content.Length > 0)
					{
						int offset = 0;
						
						ASCIIEncoding asc = new ASCIIEncoding();
						while (offset < (content.Length - 4))
						{
							int currentOffset = offset + 1;
							for (; content[currentOffset] != 0x20; currentOffset++) ;	// Find the space character
							int infoSize = currentOffset - offset;
							byte[] info = new byte[infoSize];
							Buffer.BlockCopy( content, offset, info, 0, infoSize );

							int messageNumber = Convert.ToInt32( asc.GetString( info ), System.Globalization.CultureInfo.InvariantCulture );

							currentOffset += 1;

							offset = currentOffset++;
							for (; (content[currentOffset] >= 0x30) && (content[currentOffset] <= 0x39); currentOffset++) ;	// Find the last number

							infoSize = currentOffset - offset;
							info = new byte[infoSize];
							Buffer.BlockCopy( content, offset, info, 0, infoSize );

							int messageSize = Convert.ToInt32( asc.GetString( info ), System.Globalization.CultureInfo.InvariantCulture );
							offset = currentOffset + 1;
							for (; content[offset] != 0xA; offset++) ;

							b_MailMessages.Add( new Pop3Message( messageNumber, messageSize, this ) );
						} 
					}
				}

				return b_MailMessages;
			}
		}
		
		#endregion

		#region Destructor
		~Pop3Client()
		{
			Dispose( false );
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Attempts to authorize a maildrop. TODO: Add APOP support and make it the default authentication method.
		/// </summary>
		/// <param name="user">User name to authorize with.</param>
		/// <param name="password">Password to authorize with.</param>
		public void Authorize( string user, string password )
		{
			if (State != Pop3State.Authorization)
				throw new ArgumentOutOfRangeException( "Not in AUTHORIZATION state" );		// TODO: Use an appropriate exception
			if (String.IsNullOrEmpty( user ))
				throw new ArgumentNullException( "user" );
			if (String.IsNullOrEmpty( password ))
				throw new ArgumentNullException( "password" );

			byte[] content = null;

			SendCommand( Pop3Command.User, user );
			content = popStream.Read();

			if (content[0] != 0x2b)
				throw new UserNotAcceptedException( ParseError( content ) );

			SendCommand( Pop3Command.Password, password );
			content = popStream.Read();

			if (content[0] != 0x2b)
				throw new UserNotAcceptedException( ParseError( content ) );

			State = Pop3State.Transaction;
		}

		/// <summary>
		/// Marks the specified message for deletion.
		/// </summary>
		/// <param name="messageNumber">Number of the message to delete (the same thing as SectorPoint.Net.Mail.Pop3Message.MessageNumber).</param>
		public void DeleteMessage( int messageNumber )
		{
			SendCommand( Pop3Command.Delete, messageNumber.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
		}

		/// <summary>
		/// Releases all resources used by the SectorPoint.Net.Mail.Pop3Client.
		/// </summary>
		public void Dispose()
		{
			Dispose( true );
			GC.SuppressFinalize( this );
		}

		/// <summary>
		/// Validates the SSL certificate received from the remote server (callback for the System.Net.Security.SslStream constructor).
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="certificate"></param>
		/// <param name="chain"></param>
		/// <param name="sslPolicyErrors"></param>
		/// <returns></returns>
		public static bool ValidateServerCertificate( object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors )
		{
			if (sslPolicyErrors == SslPolicyErrors.None)
				return true;

			return false;
		}
		#endregion

		#region Private Methods
		/// <summary>
		/// Releases all resources used by the SectorPoint.Net.Mail.Pop3Client.
		/// </summary>
		/// <param name="disposing">True if calling this method from the user's code (managed and unmanaged resources can be disposed). False if this is being called from inside the finalizer (only dispose unmanaged resources).</param>
		void Dispose( bool disposing )
		{
			if (disposing)
			{
				if (popStream != null)
					popStream.Dispose();

				if (tcpClient != null)
					tcpClient.Close();
			}
		}

		/// <summary>
		/// Establishes a connection to the specified server and verifies the greeting to ensure that we are connected to a POP3 server.
		/// </summary>
		void Initialize()
		{
			tcpClient = new TcpClient( host, port );
			
			if (!useSsl)
			{
				popStream = new Pop3Stream( tcpClient.GetStream() );
			}
			else
			{
				SslStream sslStream = new SslStream( tcpClient.GetStream(), false, new RemoteCertificateValidationCallback( ValidateServerCertificate ), null );
				sslStream.AuthenticateAsClient( host );
				sslStream.ReadTimeout = 5000;
				popStream = new Pop3Stream( sslStream );
			}
			if (!this.OkResponse())
				throw new ApplicationException( "Not valid POP server" );		// TODO: cleanup error message
		}

		bool OkResponse()
		{
			byte[] content = new byte[512];
			int responseLength = popStream.Read( content, 0, 512 );
			if (responseLength == 0)
				return false;

			int firstChar = content[0];
			if (firstChar == 0x2b)
				return true;
			else if (firstChar == 0x2d)
				return false;

			// If we've gotten to this point, the server is sending a line of garbage;

			for (int i = 1; i <= responseLength; i++)
			{
				if (content[i] == 0x2b)
					return true;
				if (content[i] == 0x2d)
					return false;
			}

			return false;
		}

		/// <summary>
		/// Parses an error response from the server. Trims out the "-ERR " and returns the rest of the string.
		/// </summary>
		/// <param name="content">Content of the response from the server.</param>
		/// <returns>An error message suitable for displaying to the user.</returns>
		static string ParseError( byte[] content )
		{
			int contentLength = content.Length;
			StringBuilder errorMessage = new StringBuilder( contentLength - 4 );
			for (int i = 5; i < contentLength; i++)
			{
				if (content[i] == 0)
					break;

				errorMessage.Append( (char)content[i] );
			}

			return errorMessage.ToString();
		}

		/// <summary>
		/// Sends a command string to the POP3 server.
		/// </summary>
		/// <param name="command">Command to send.</param>
		void SendCommand( string command )
		{
			int commandLength = command.Length;
			ASCIIEncoding asc = new ASCIIEncoding();
			byte[] buffer = asc.GetBytes( command );
			popStream.Write( buffer, 0, commandLength );
		}
		#endregion

		#region Internal Methods
		/// <summary>
		/// Sends the specified command to the POP3 server.
		/// </summary>
		/// <param name="command">Command to send.</param>
		internal void SendCommand( Pop3Command command )
		{
			SendCommand( (string)TypeDescriptor.GetConverter( typeof( Pop3Command ) ).ConvertTo( command, typeof( string ) ) + "\r\n" );
		}

		/// <summary>
		/// Sends the specified command to the POP3 server.
		/// </summary>
		/// <param name="command">Command to send.</param>
		/// <param name="argument">Argument to send.</param>
		internal void SendCommand( Pop3Command command, string argument )
		{
			SendCommand( (string)TypeDescriptor.GetConverter( typeof( Pop3Command ) ).ConvertTo( command, typeof( string ) ) + @" " + argument + "\r\n" );

		}

		internal byte[] Read()
		{
			return popStream.Read( true, true, true );
		}
		#endregion
	}
}
